package sesi.ejava.stream;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;

import sesi.ejava.bcam.R;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;

/**
 * This singleton class provides the service to connect to a bluetooth device as
 * client and also provides the function to read and send data to and from the
 * connected device.
 * 
 * @author Cesar Fuguet
 */
public class BluetoothManagerService {
	// Constants
	private final UUID MY_UUID = UUID
			.fromString("fa87c0d0-afac-11de-8a39-0800200c9a66");

	// Message String Keys
	public static final String TOAST = "toast";
	public static final String DEVICE_NAME = "device_name";

	// Message types
	public static final int MESSAGE_TOAST = 0;
	public static final int MESSAGE_DEVICE_NAME = 1;
	public static final int DATA_READ = 0;

	// Connection States
	public static final int STATE_IDLE = 0;
	public static final int STATE_CONNECTING = 1;
	public static final int STATE_CONNECTED = 2;

	// Member variables
	private final BluetoothAdapter bluetoothAdapter;
	private Handler handler;
	private ConnectThread connectThread;
	private ConnectedThread connectedThread;
	private Context context;
	private int connectionState;
	private String connectedDevice;

	private static BluetoothManagerService instance = null;

	private BluetoothManagerService() {
		bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
		connectionState = STATE_IDLE;
		connectedDevice = null;
	}

	public static synchronized BluetoothManagerService getInstance() {
		if (instance == null) {
			instance = new BluetoothManagerService();
		}

		return instance;
	}

	public void setContext(Context context) {
		this.context = context;
	}

	public void setHandler(Handler handler) {
		this.handler = handler;
	}

	public String getConnectedDevice() {
		return connectedDevice;
	}

	public boolean isConnected() {
		return (connectionState == STATE_CONNECTED);
	}

	/**
	 * Indicate that the connection attempt failed and notify the UI Activity.
	 */
	private void connectionFailed() {
		// Send a failure message back to the Activity
		Message msg = handler.obtainMessage(MESSAGE_TOAST);

		Bundle bundle = new Bundle();
		bundle.putString(TOAST, context.getString(R.string.connectionFailed));

		msg.setData(bundle);
		handler.sendMessage(msg);
	}

	/**
	 * Indicate that the connection was lost and notify the UI Activity.
	 */
	private void connectionLost() {
		setState(STATE_IDLE);

		// Send a failure message back to the Activity
		Message msgHandler = handler.obtainMessage(MESSAGE_TOAST);

		Bundle bundle = new Bundle();
		bundle.putString(BluetoothManagerService.TOAST,
				"Device connection was lost");

		handler.sendMessage(msgHandler);
	}

	public synchronized void connect(BluetoothDevice device) {
		// Cancel any thread attempting to make a connection
		if (connectionState == STATE_CONNECTING) {
			if (connectThread != null) {
				connectThread.cancel();
				connectThread = null;
			}
		}

		// Cancel any thread currently running a connection
		if (connectedThread != null) {
			connectedThread.cancel();
			connectedThread = null;
		}

		// Start the thread to connect with the given device
		connectThread = new ConnectThread(device);
		connectThread.start();
		setState(STATE_CONNECTING);
	}

	/**
	 * Start the ConnectedThread to begin managing a Bluetooth connection
	 * 
	 * @param socket
	 *            The BluetoothSocket on which the connection was made
	 * @param device
	 *            The BluetoothDevice that has been connected
	 */
	public synchronized void connected(BluetoothSocket socket,
			BluetoothDevice device) {
		// Cancel the thread that completed the connection
		if (connectThread != null) {
			connectThread.cancel();
			connectThread = null;
		}

		// Cancel any thread currently running a connection
		if (connectedThread != null) {
			connectedThread.cancel();
			connectedThread = null;
		}

		// Start the thread to manage the connection and perform transmissions
		connectedThread = new ConnectedThread(socket);
		connectedThread.start();

		// Send the name of the connected device back to the UI Activity
		Message msg = handler.obtainMessage(MESSAGE_DEVICE_NAME);

		Bundle bundle = new Bundle();
		connectedDevice = device.getName();
		bundle.putString(DEVICE_NAME, connectedDevice);

		msg.setData(bundle);
		handler.sendMessage(msg);

		setState(STATE_CONNECTED);
	}

	/**
	 * Write to the ConnectedThread in an unsynchronized manner
	 * 
	 * @param out
	 *            The bytes to write
	 * @see ConnectedThread#write(byte[])
	 */
	public void write(byte[] out) {
		// Create temporary object
		ConnectedThread r;
		// Synchronize a copy of the ConnectedThread
		synchronized (this) {
			if (connectionState != STATE_CONNECTED)
				return;
			r = connectedThread;
		}
		// Perform the write unsynchronized
		r.write(out);
	}

	/**
	 * Return the current connection state.
	 */
	public synchronized int getState() {
		return connectionState;
	}

	/**
	 * Set the current state of the chat connection
	 * 
	 * @param state
	 *            An integer defining the current connection state
	 */
	private synchronized void setState(int state) {
		connectionState = state;
	}

	/**
	 * This thread runs while attempting to make an outgoing connection with a
	 * device. It runs straight through; the connection either succeeds or
	 * fails.
	 */
	private class ConnectThread extends Thread {
		private final BluetoothSocket bluetoothSocket;
		private final BluetoothDevice bluetoothDevice;

		public ConnectThread(BluetoothDevice device) {
			BluetoothSocket tmpSocket = null;
			bluetoothDevice = device;

			// Get a BluetoothSocket for a connection with the
			// given BluetoothDevice
			try {
				tmpSocket = device.createRfcommSocketToServiceRecord(MY_UUID);
			} catch (IOException e) {
				Log.e("BluetoothManagerService.ConnectThread",
						"create() failed", e);
			}

			bluetoothSocket = tmpSocket;
		}

		public void run() {
			Log.i("BluetoothManagerService.ConnectThread",
					"BEGIN connectThread");
			setName("ConnectThread");

			// Always cancel discovery because it will slow down a connection
			bluetoothAdapter.cancelDiscovery();

			// Make a connection to the BluetoothSocket
			try {
				// This is a blocking call and will only return on a
				// successful connection or an exception
				bluetoothSocket.connect();

			} catch (IOException e) {
				connectionFailed();

				// Close the socket
				try {
					bluetoothSocket.close();
				} catch (IOException e2) {
					Log.e("BluetoothManagerService.ConnectThread",
							"unable to close() socket during connection failure",
							e2);
				}

				return;
			}

			// Reset the ConnectThread because we're done
			synchronized (BluetoothManagerService.this) {
				connectThread = null;
			}

			// Start the connected thread
			connected(bluetoothSocket, bluetoothDevice);
		}

		public void cancel() {
			try {
				bluetoothSocket.close();
			} catch (IOException e) {
				Log.e("BluetoothManagerService.ConnectThread",
						"close() of connect socket failed", e);
			}
		}
	}

	/**
	 * This thread runs during a connection with a remote device. It handles all
	 * incoming and outgoing transmissions.
	 */
	private class ConnectedThread extends Thread {
		private final BluetoothSocket bluetoothSocket;
		private final InputStream inputStream;
		private final OutputStream outputStream;

		public ConnectedThread(BluetoothSocket bluetoothSocket) {
			Log.d("BluetoothManagerService.ConnectedThread",
					"create ConnectedThread");
			this.bluetoothSocket = bluetoothSocket;
			InputStream tmpInputStream = null;
			OutputStream tmpOutputStream = null;

			// Get the BluetoothSocket input and output streams
			try {
				tmpInputStream = this.bluetoothSocket.getInputStream();
				tmpOutputStream = this.bluetoothSocket.getOutputStream();
			} catch (IOException e) {
				Log.e("BluetoothManagerService.ConnectedThread",
						"temp sockets not created", e);
			}

			inputStream = tmpInputStream;
			outputStream = tmpOutputStream;
		}

		public void run() {
			byte[] buffer = new byte[1024];
			int bytes;

			// Keep listening to the InputStream while connected
			while (true) {
				try {
					// Read from the InputStream
					bytes = inputStream.read(buffer);

					// Send the obtained bytes to the UI Activity
					if (handler != null) {
						Log.i("ConnectedThread", "Bytestream readed");
						handler.obtainMessage(DATA_READ, bytes, -1, buffer)
								.sendToTarget();
					}
				} catch (IOException e) {
					Log.d("Connection", "ConnectionLost");
					connectionLost();
					break;
				}
			}
		}

		/**
		 * Write to the connected OutStream.
		 * 
		 * @param buffer
		 *            The bytes to write
		 */
		public void write(byte[] buffer) {
			try {
				Log.i("ConnectedThread", "Bytestream sended");
				outputStream.write(buffer);
			} catch (IOException e) {
				Log.e("BluetoothManagerService.ConnectedThread",
						"Exception during write", e);
			}
		}

		public void cancel() {
			try {
				bluetoothSocket.close();
			} catch (IOException e) {
				Log.e("BluetoothManagerService.ConnectedThread",
						"close() of connect socket failed", e);
			}
		}
	}
}
